[Chapter Eighteen][Previous]
[Next] [Art of
Assembly][Randall Hyde]
Art of Assembly: Chapter Eighteen
- 18.3 - Reentrancy
- 18.3.1 - Reentrancy Problems with DOS
- 18.3.2 - Reentrancy Problems with BIOS
- 18.3.3 - Reentrancy Problems with Other
Code
- 18.4 - The Multiplex Interrupt (INT 2Fh)
18.3 Reentrancy
One big problem with active TSRs is that their invocation is asynchronous.
They can activate at the touch of a keystroke, timer interrupt, or via an
incoming character on the serial port, just to name a few. Since they activate
on a hardware interrupt, the PC could have been executing just about any
code when the interrupt came along. This isn't a problem unless the TSR
itself decides to call some foreign code, such as DOS, a BIOS routine, or
some other TSR. For example, the main application may be making a DOS call
when a timer interrupt activates a TSR, interrupting the call to DOS while
the CPU is still executing code inside DOS. If the TSR attempts to make
a call to DOS at this point, then this will reenter DOS. Of course, DOS
is not reentrant, so this creates all kinds of problems (usually, it hangs
the system). When writing active TSRs that call other routines besides those
provided directly in the TSR, you must be aware of possible reentrancy problems.
Note that passive TSRs never suffer from this problem. Indeed, any TSR routine
you call passively will execute in the caller's environment. Unless some
other hardware ISR or active TSR makes the call to your routine, you do
not need to worry about reentrancy with passive routines. However, reentrancy
is an issue for active TSR routines and passive routines that active TSRs
call.
18.3.1 Reentrancy Problems with DOS
DOS is probably the biggest sore point to TSR developers. DOS is not
reentrant yet DOS contains many services a TSR might use. Realizing this,
Microsoft has added some support to DOS to allow TSRs to see if DOS is currently
active. After all, reentrancy is only a problem if you call DOS while it
is already active. If it isn't already active, you can certainly call it
from a TSR with no ill effects.
MS-DOS provides a special one-byte flag ( InDOS) that contains a zero if
DOS is currently active and a non-zero value if DOS is already processing
an application request. By testing the InDOS flag your TSR can determine
if it can safely make a DOS call. If this flag is zero, you can always make
the DOS call. If this flag contains one, you may not be able to make the
DOS call. MS-DOS provides a function call, Get InDOS Flag Address, that
returns the address of the InDOS flag. To use this function, load ah
with 34h and call DOS. DOS will return the address of the InDOS flag in
es:bx
. If you save this address, your resident programs will
be able to test the InDOS flag to see if DOS is active.
Actually, there are two flags you should test, the InDOS flag and the critical
error flag (criterr). Both of these flags should contain zero before you
call DOS from a TSR. In DOS version 3.1 and later, the critical error flag
appears in the byte just before the InDOS flag.
So what should you do if these flags aren't both zero? It's easy enough
to say "hey, come back and do this stuff later when MS-DOS returns
back to the user program." But how do you do this? For example, if
a keyboard interrupt activates your TSR and you pass control on to the real
keyboard handler because DOS is busy, you can't expect your TSR to be magically
restarted later on when DOS is no longer active.
The trick is to patch your TSR into the timer interrupt as well as the keyboard
interrupt. When the keystroke interrupt wakes your TSR and you discover
that DOS is busy, the keyboard ISR can simply set a flag to tell itself
to try again later; then it passes control to the original keyboard handler.
In the meantime, a timer ISR you've written is constantly checking this
flag you've created. If the flag is clear, it simply passes control on to
the original timer interrupt handler, if the flag is set, then the code
checks the InDOS and CritErr flags. If these guys say that DOS is busy,
the timer ISR passes control on to the original timer handler. Shortly after
DOS finishes whatever it was doing, a timer interrupt will come along and
detect that DOS is no longer active. Now your ISR can take over and make
any necessary calls to DOS that it wants. Of course, once your timer code
determines that DOS is not busy, it should clear the "I want service"
flag so that future timer interrupts don't inadvertently restart the TSR.
There is only one problem with this approach. There are certain DOS calls
that can take an indefinite amount of time to execute. For example, if you
call DOS to read a key from the keyboard (or call the Standard Library's
getc
routine that calls DOS to read a key), it could be hours,
days, or even longer before somebody actually bothers to press a key. Inside
DOS there is a loop that waits until the user actually presses a key. And
until the user presses some key, the InDOS flag is going to remain non-zero.
If you've written a timer-based TSR that is buffering data every few seconds
and needs to write the results to disk every now and then, you will overflow
your buffer with new data if you wait for the user, who just went to lunch,
to press a key in DOS' command.com program.
Luckily, MS-DOS provides a solution to this problem as well - the idle interrupt.
While MS-DOS is in an indefinite loop wait for an I/O device, it continually
executes an int 28h i
nstruction. By patching into the int 28h
vector, your TSR can determine when DOS is sitting in such a loop. When
DOS executes the int 28h instruction, it is safe to make any DOS call whose
function number (the value in ah
) is greater than 0Ch.
So if DOS is busy when your TSR wants to make a DOS call, you must use either
a timer interrupt or the idle interrupt (int 28h) to activate the portion
of your TSR that must make DOS calls. One final thing to keep in mind is
that whenever you test or modify any of the above mentioned flags, you are
in a critical section. Make sure the interrupts are off. If not, your TSR
make activate two copies of itself or you may wind up entering DOS at the
same time some other TSR enters DOS.
An example of a TSR using these techniques will appear a little later, but
there are some additional reentrancy problems we need to discuss first.
18.3.2 Reentrancy Problems with BIOS
DOS isn't the only non-reentrant code a TSR might want to call. The
PC's BIOS routines also fall into this category. Unfortunately, BIOS doesn't
provide an "InBIOS" flag or a multiplex interrupt. You will have
to supply such functionality yourself.
The key to preventing reentering a BIOS routine you want to call is to use
a wrapper. A wrapper is a short ISR that patches into an existing BIOS interrupt
specifically to manipulate an InUse flag. For example, suppose you need
to make an int 10h (video services) call from within your TSR. You could
use the following code to provide an "Int10InUse" flag that your
TSR could test:
MyInt10 proc far
inc cs:Int10InUse
pushf
call cs:OldInt10
dec cs:Int10InUse
iret
MyInt10 endp
Assuming you've initialized the Int10InUse variable to zero, the in use
flag will contain zero when it is safe to execute an int 10h instruction
in your TSR, it will contain a non-zero value when the interrupt 10h handler
is busy. You can use this flag like the InDOS flag to defer the execution
of your TSR code.
Like DOS, there are certain BIOS routines that may take an indefinite amount
of time to complete. Reading a key from the keyboard buffer, reading or
writing characters on the serial port, or printing characters to the printer
are some examples. While, in some cases, it is possible to create a wrapper
that lets your TSR activate itself while a BIOS routine is executing one
of these polling loops, there is probably no benefit to doing so. For example,
if an application program is waiting for the printer to take a character
before it sends another to printer, having your TSR preempt this and attempt
to send a character to the printer won't accomplish much (other than scramble
the data sent to the print). Therefore, BIOS wrappers generally don't worry
about indefinite postponement in a BIOS routine.
5, 8, 9, D, E, 10, 13, 16, 17, 21, 28
If you run into problems with your TSR code and certain application programs,
you may want to place wrappers around the following interrupts to see if
this solves your problem: int 5, int 8, int 9, int B, int C, int D, int
E, int 10, int 13, int 14, int 16, or int 17. These are common culprits
when TSR problems develop.
18.3.3 Reentrancy Problems with Other Code
Reentrancy problems occur in other code you might call as well. For
example, consider the UCR Standard Library. The UCR Standard Library is
not reentrant. This usually isn't much of a problem for a couple of reasons.
First, most TSRs do not call Standard Library subroutines. Instead, they
provide results that normal applications can use; those applications use
the Standard Library routines to manipulate such results. A second reason
is that were you to include some Standard Library routines in a TSR, the
application would have a separate copy of the library routines. The TSR
might execute an strcmp instruction while the application is in the middle
of an strcmp routine, but these are not the same routines! The TSR is not
reentering the application's code, it is executing a separate routine.
However, many of the Standard Library functions make DOS or BIOS calls.
Such calls do not check to see if DOS or BIOS is already active. Therefore,
calling many Standard Library routines from within a TSR may cause you to
reenter DOS or BIOS.
One situation does exist where a TSR could reenter a Standard Library routine.
Suppose your TSR has both passive and active components. If the main application
makes a call to a passive routine in your TSR and that routine call a Standard
Library routine, there is the possibility that a system interrupt could
interrupt the Standard Library routine and the active portion of the TSR
reenter that same code. Although such a situation would be extremely rare,
you should be aware of this possibility.
Of course, the best solution is to avoid using the Standard Library within
your TSRs. If for no other reason, the Standard Library routines are quite
large and TSRs should be as small as possible.
18.4 The Multiplex Interrupt (INT 2Fh)
When installing a passive TSR, or an active TSR with passive components,
you will need to choose some interrupt vector to patch so other programs
can communicate with your passive routines. You could pick an interrupt
vector almost at random, say int 84h, but this could lead to some compatibility
problems. What happens if someone else is already using that interrupt vector?
Sometimes, the choice of interrupt vector is clear. For example, if your
passive TSR is extended the int 16h keyboard services, it makes sense to
patch in to the int 16h vector and add additional functions above and beyond
those already provided by the BIOS. On the other hand, if you are creating
a driver for some brand new device for the PC, you probably would not want
to piggyback the support functions for this device on some other interrupt.
Yet arbitrarily picking an unused interrupt vector is risky; how many other
programs out there decided to do the same thing? Fortunately, MS-DOS provides
a solution: the multiplex interrupt. Int 2Fh provides a general mechanism
for installing, testing the presence of, and communicating with a TSR.
To use the multiplex interrupt, an application places an identification
value in ah
and a function number in al
and then
executes an int 2Fh
instruction. Each TSR in the int 2Fh chain
compares the value in ah
against its own unique identifier
value. If the values match, the TSR process the command specified by the
value in the al
register. If the identification values do not
match, the TSR passes control to the next int 2Fh handler in the chain.
Of course, this only reduces the problem somewhat, it doesn't eliminate
it. Sure, we don't have to guess an interrupt vector number at random, but
we still have to choose a random identification number. After all, it seems
reasonable that we must choose this number before designing the TSR and
any applications that call it, after all, how will the applications know
what value to load into ah
if we dynamically assign this value
when the TSR goes resident?
Well, there is a little trick we can play to dynamically assign TSR identifiers
and let any interested applications determine the TSR's ID. By convention,
function zero is the "Are you there?" call. An application should
always execute this function to determine if the TSR is actually present
in memory before making any service requests. Normally, function zero returns
a zero in al if the TSR is not present, it returns 0FFh if it is present.
However, when this function returns 0FFh it only tells you that some TSR
has responded to your query; it does not guarantee that the TSR you are
interested in is actually present in memory. However, by extending the convention
somewhat, it is very easy to verify the presence of the desired TSR. Suppose
the function zero call also returns a pointer to a unique identification
string in the es:di
registers. Then the code testing for the
presence of a specific TSR could test this string when the int 2Fh call
detects the presence of a TSR. the following code segment demonstrates how
a TSR could determine if a TSR identified as "Randy's INT 10h Extension"
is present in memory; this code will also determine the unique identification
code for that TSR, for future reference:
; Scan through all the possible TSR IDs. If one is installed, see if
; it's the TSR we're interested in.
mov cx, 0FFh ;This will be the ID number.
IDLoop: mov ah, cl ;ID -> AH.
push cx ;Preserve CX across call
mov al, 0 ;Test presence function code.
int 2Fh ;Call multiplex interrupt.
pop cx ;Restore CX.
cmp al, 0 ;Installed TSR?
je TryNext ;Returns zero if none there.
strcmpl ;See if it's the one we want.
byte "Randy's INT "
byte "10h Extension",0
je Success ;Branch off if it is ours.
TryNext: loop IDLoop ;Otherwise, try the next one.
jmp NotInstalled ;Failure if we get to this point.
Success: mov FuncID, cl ;Save function result.
.
.
.
If this code succeeds, the variable FuncId contains the identification value
for resident TSR. If it fails, the application program probably needs to
abort, or otherwise ensure that it never calls the missing TSR.
The code above lets an application easily detect the presence of and determine
the ID number for a specific TSR. The next question is "How do we pick
the ID number for the TSR in the first place?" The next section will
address that issue, as well as how the TSR must respond to the multiplex
interrupt.
- 18.3 - Reentrancy
- 18.3.1 - Reentrancy Problems with DOS
- 18.3.2 - Reentrancy Problems with BIOS
- 18.3.3 - Reentrancy Problems with Other
Code
- 18.4 - The Multiplex Interrupt (INT 2Fh)
Art of Assembly: Chapter Eighteen - 29 SEP 1996
[Chapter Eighteen][Previous]
[Next] [Art of
Assembly][Randall Hyde]